/*
  +----------------------------------------------------------------------+
  | Copyright (c) The PHP Group                                          |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | https://www.php.net/license/3_01.txt                                 |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Authors: Edin Kadribasic <edink@emini.dk>                            |
  |          Ilia Alshanestsky <ilia@prohost.org>                        |
  |          Wez Furlong <wez@php.net>                                   |
  +----------------------------------------------------------------------+
*/

#include "cat_pq.h"

#ifdef CAT_PQ

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "ext/standard/php_string.h"
#include "main/php_network.h"
#include "pdo/php_pdo.h"
#include "pdo/php_pdo_driver.h"
#include "pdo/php_pdo_error.h"
#include "ext/standard/file.h"
#undef SIZEOF_OFF_T
#include "swow_pdo_pgsql_int.h"
#include "zend_exceptions.h"

#undef ZEND_METHOD
#define ZEND_METHOD(classname, name) ZEND_NAMED_FUNCTION(swow_zim_##classname##_##name)
#undef ZEND_ME
#if PHP_VERSION_ID >= 80400
# define ZEND_ME(classname, name, arg_info, flags) ZEND_RAW_FENTRY(#name, swow_zim_##classname##_##name, arg_info, flags, NULL, NULL)
#else
# define ZEND_ME(classname, name, arg_info, flags) ZEND_RAW_FENTRY(#name, swow_zim_##classname##_##name, arg_info, flags)
#endif

#include "swow_pgsql_driver_arginfo.h"


/* Git hash: php/php-src@0e45ed772df304c58f151d75d75f4ab5d9192c5b */
#if PHP_VERSION_ID < 80100

static int pgsql_handle_in_transaction(pdo_dbh_t *dbh);

static char * _pdo_pgsql_trim_message(const char *message, int persistent)
{
    register int i = strlen(message)-1;
    char *tmp;

    if (i>1 && (message[i-1] == '\r' || message[i-1] == '\n') && message[i] == '.') {
        --i;
    }
    while (i>0 && (message[i] == '\r' || message[i] == '\n')) {
        --i;
    }
    ++i;
    tmp = pemalloc(i + 1, persistent);
    memcpy(tmp, message, i);
    tmp[i] = '\0';

    return tmp;
}

static zend_string* _pdo_pgsql_escape_credentials(char *str)
{
    if (str) {
        return php_addcslashes_str(str, strlen(str), "\\'", sizeof("\\'"));
    }

    return NULL;
}

int _swow_pdo_pgsql_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, int errcode, const char *sqlstate, const char *msg, const char *file, int line) /* {{{ */
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_error_type *pdo_err = stmt ? &stmt->error_code : &dbh->error_code;
    pdo_pgsql_error_info *einfo = &H->einfo;
    char *errmsg = PQerrorMessage(H->server);

    einfo->errcode = errcode;
    einfo->file = file;
    einfo->line = line;

    if (einfo->errmsg) {
        pefree(einfo->errmsg, dbh->is_persistent);
        einfo->errmsg = NULL;
    }

    if (sqlstate == NULL || strlen(sqlstate) >= sizeof(pdo_error_type)) {
        strcpy(*pdo_err, "HY000");
    }
    else {
        strcpy(*pdo_err, sqlstate);
    }

    if (msg) {
        einfo->errmsg = pestrdup(msg, dbh->is_persistent);
    }
    else if (errmsg) {
        einfo->errmsg = _pdo_pgsql_trim_message(errmsg, dbh->is_persistent);
    }

    if (!dbh->methods) {
        pdo_throw_exception(einfo->errcode, einfo->errmsg, pdo_err);
    }

    return errcode;
}
/* }}} */

static void _swow_pdo_pgsql_notice(pdo_dbh_t *dbh, const char *message) /* {{{ */
{
/*    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; */
}
/* }}} */

static int swow_pdo_pgsql_fetch_error_func(pdo_dbh_t *dbh, pdo_stmt_t *stmt, zval *info) /* {{{ */
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_pgsql_error_info *einfo = &H->einfo;

    if (einfo->errcode) {
        add_next_index_long(info, einfo->errcode);
    } else {
        add_next_index_null(info);
    }
    if (einfo->errmsg) {
        add_next_index_string(info, einfo->errmsg);
    }

    return 1;
}
/* }}} */

/* {{{ pdo_pgsql_create_lob_stream */
static ssize_t swow_pgsql_lob_write(php_stream *stream, const char *buf, size_t count)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    return lo_write(self->conn, self->lfd, (char*)buf, count);
}

static ssize_t swow_pgsql_lob_read(php_stream *stream, char *buf, size_t count)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    return lo_read(self->conn, self->lfd, buf, count);
}

static int swow_pgsql_lob_close(php_stream *stream, int close_handle)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)(Z_PDO_DBH_P(&self->dbh))->driver_data;

    if (close_handle) {
        lo_close(self->conn, self->lfd);
    }
    zend_hash_index_del(H->lob_streams, php_stream_get_resource_id(stream));
    zval_ptr_dtor(&self->dbh);
    efree(self);
    return 0;
}

static int swow_pgsql_lob_flush(php_stream *stream)
{
    return 0;
}

static int swow_pgsql_lob_seek(php_stream *stream, zend_off_t offset, int whence,
        zend_off_t *newoffset)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
#if defined(HAVE_PG_LO64) && defined(ZEND_ENABLE_ZVAL_LONG64)
    zend_off_t pos = lo_lseek64(self->conn, self->lfd, offset, whence);
#else
    zend_off_t pos = lo_lseek(self->conn, self->lfd, offset, whence);
#endif
    *newoffset = pos;
    return pos >= 0 ? 0 : -1;
}

const php_stream_ops swow_pdo_pgsql_lob_stream_ops = {
    swow_pgsql_lob_write,
    swow_pgsql_lob_read,
    swow_pgsql_lob_close,
    swow_pgsql_lob_flush,
    "pdo_pgsql lob stream",
    swow_pgsql_lob_seek,
    NULL,
    NULL,
    NULL
};

php_stream *swow_pdo_pgsql_create_lob_stream(zval *dbh, int lfd, Oid oid)
{
    php_stream *stm;
    struct pdo_pgsql_lob_self *self = ecalloc(1, sizeof(*self));
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)(Z_PDO_DBH_P(dbh))->driver_data;

    ZVAL_COPY_VALUE(&self->dbh, dbh);
    self->lfd = lfd;
    self->oid = oid;
    self->conn = H->server;

    stm = php_stream_alloc(&swow_pdo_pgsql_lob_stream_ops, self, 0, "r+b");

    if (stm) {
        Z_ADDREF_P(dbh);
        zend_hash_index_add_ptr(H->lob_streams, php_stream_get_resource_id(stm), stm->res);
        return stm;
    }

    efree(self);
    return NULL;
}
/* }}} */

void swow_pdo_pgsql_close_lob_streams(pdo_dbh_t *dbh)
{
    zend_resource *res;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (H->lob_streams) {
        ZEND_HASH_REVERSE_FOREACH_PTR(H->lob_streams, res) {
            if (res->type >= 0) {
                zend_list_close(res);
            }
        } ZEND_HASH_FOREACH_END();
    }
}

static int pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (H) {
        if (H->lob_streams) {
            swow_pdo_pgsql_close_lob_streams(dbh);
            zend_hash_destroy(H->lob_streams);
            pefree(H->lob_streams, dbh->is_persistent);
            H->lob_streams = NULL;
        }
        if (H->server) {
            PQfinish(H->server);
            H->server = NULL;
        }
        if (H->einfo.errmsg) {
            pefree(H->einfo.errmsg, dbh->is_persistent);
            H->einfo.errmsg = NULL;
        }
        pefree(H, dbh->is_persistent);
        dbh->driver_data = NULL;
    }
    return 0;
}
/* }}} */

static int pgsql_handle_preparer(pdo_dbh_t *dbh, const char *sql, size_t sql_len, pdo_stmt_t *stmt, zval *driver_options)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_pgsql_stmt *S = ecalloc(1, sizeof(pdo_pgsql_stmt));
    int scrollable;
    int ret;
    char *nsql = NULL;
    size_t nsql_len = 0;
    int emulate = 0;
    int execute_only = 0;

    S->H = H;
    stmt->driver_data = S;
    stmt->methods = &swow_pgsql_stmt_methods;

    scrollable = pdo_attr_lval(driver_options, PDO_ATTR_CURSOR,
        PDO_CURSOR_FWDONLY) == PDO_CURSOR_SCROLL;

    if (scrollable) {
        if (S->cursor_name) {
            efree(S->cursor_name);
        }
        spprintf(&S->cursor_name, 0, "pdo_crsr_%08x", ++H->stmt_counter);
        emulate = 1;
    } else if (driver_options) {
        if (pdo_attr_lval(driver_options, PDO_ATTR_EMULATE_PREPARES, H->emulate_prepares) == 1) {
            emulate = 1;
        }
        if (pdo_attr_lval(driver_options, PDO_PGSQL_ATTR_DISABLE_PREPARES, H->disable_prepares) == 1) {
            execute_only = 1;
        }
    } else {
        emulate = H->disable_native_prepares || H->emulate_prepares;
        execute_only = H->disable_prepares;
    }

    if (!emulate && PQprotocolVersion(H->server) <= 2) {
        emulate = 1;
    }

    if (emulate) {
        stmt->supports_placeholders = PDO_PLACEHOLDER_NONE;
    } else {
        stmt->supports_placeholders = PDO_PLACEHOLDER_NAMED;
        stmt->named_rewrite_template = "$%d";
    }

    ret = pdo_parse_params(stmt, (char*)sql, sql_len, &nsql, &nsql_len);

    if (ret == -1) {
        /* couldn't grok it */
        strcpy(dbh->error_code, stmt->error_code);
        return 0;
    } else if (ret == 1) {
        /* query was re-written */
        S->query = nsql;
    } else {
        S->query = estrdup(sql);
    }

    if (!emulate && !execute_only) {
        /* prepared query: set the query name and defer the
           actual prepare until the first execute call */
        spprintf(&S->stmt_name, 0, "pdo_stmt_%08x", ++H->stmt_counter);
    }

    return 1;
}

static zend_long pgsql_handle_doer(pdo_dbh_t *dbh, const char *sql, size_t sql_len)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    PGresult *res;
    zend_long ret = 1;
    ExecStatusType qs;

    bool in_trans = pgsql_handle_in_transaction(dbh);

    if (!(res = cat_pq_exec(H->server, sql))) {
        /* fatal error */
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
        return -1;
    }
    qs = PQresultStatus(res);
    if (qs != PGRES_COMMAND_OK && qs != PGRES_TUPLES_OK) {
        pdo_pgsql_error(dbh, qs, pdo_pgsql_sqlstate(res));
        PQclear(res);
        return -1;
    }
    H->pgoid = PQoidValue(res);
    if (qs == PGRES_COMMAND_OK) {
        ZEND_ATOL(ret, PQcmdTuples(res));
    } else {
        ret = Z_L(0);
    }
    PQclear(res);
    if (in_trans && !pgsql_handle_in_transaction(dbh)) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    }

    return ret;
}

static int pgsql_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unquotedlen, char **quoted, size_t *quotedlen, enum pdo_param_type paramtype)
{
    unsigned char *escaped;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    size_t tmp_len;

    switch (paramtype) {
        case PDO_PARAM_LOB:
            /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */
            escaped = PQescapeByteaConn(H->server, (unsigned char *)unquoted, unquotedlen, &tmp_len);
            *quotedlen = tmp_len + 1;
            *quoted = emalloc(*quotedlen + 1);
            memcpy((*quoted)+1, escaped, *quotedlen-2);
            (*quoted)[0] = '\'';
            (*quoted)[*quotedlen-1] = '\'';
            (*quoted)[*quotedlen] = '\0';
            PQfreemem(escaped);
            break;
        default:
            *quoted = safe_emalloc(2, unquotedlen, 3);
            (*quoted)[0] = '\'';
            *quotedlen = PQescapeStringConn(H->server, *quoted + 1, unquoted, unquotedlen, NULL);
            (*quoted)[*quotedlen + 1] = '\'';
            (*quoted)[*quotedlen + 2] = '\0';
            *quotedlen += 2;
    }
    return 1;
}

static char *pdo_pgsql_last_insert_id(pdo_dbh_t *dbh, const char *name, size_t *len)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    char *id = NULL;
    PGresult *res;
    ExecStatusType status;

    if (name == NULL) {
        res = cat_pq_exec(H->server, "SELECT LASTVAL()");
    } else {
        const char *q[1];
        q[0] = name;

        res = cat_pq_exec_params(H->server, "SELECT CURRVAL($1)", 1, NULL, q, NULL, NULL, 0);
    }
    status = PQresultStatus(res);

    if (res && (status == PGRES_TUPLES_OK)) {
        id = estrdup((char *)PQgetvalue(res, 0, 0));
        *len = PQgetlength(res, 0, 0);
    } else {
        pdo_pgsql_error(dbh, status, pdo_pgsql_sqlstate(res));
    }

    if (res) {
        PQclear(res);
    }

    return id;
}

void swow_pdo_libpq_version(char *buf, size_t len)
{
    int version = PQlibVersion();
    int major = version / 10000;
    if (major >= 10) {
        int minor = version % 10000;
        snprintf(buf, len, "%d.%d", major, minor);
    } else {
        int minor = version / 100 % 100;
        int revision = version % 100;
        snprintf(buf, len, "%d.%d.%d", major, minor, revision);
    }
}

static int pdo_pgsql_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_value)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    switch (attr) {
        case PDO_ATTR_EMULATE_PREPARES:
            ZVAL_BOOL(return_value, H->emulate_prepares);
            break;

        case PDO_PGSQL_ATTR_DISABLE_PREPARES:
            ZVAL_BOOL(return_value, H->disable_prepares);
            break;

        case PDO_ATTR_CLIENT_VERSION: {
            char buf[16];
            swow_pdo_libpq_version(buf, sizeof(buf));
            ZVAL_STRING(return_value, buf);
            break;
        }

        case PDO_ATTR_SERVER_VERSION:
            if (PQprotocolVersion(H->server) >= 3) { /* PostgreSQL 7.4 or later */
                ZVAL_STRING(return_value, (char*)PQparameterStatus(H->server, "server_version"));
            } else /* emulate above via a query */
            {
                PGresult *res = cat_pq_exec(H->server, "SELECT VERSION()");
                if (res && PQresultStatus(res) == PGRES_TUPLES_OK) {
                    ZVAL_STRING(return_value, (char *)PQgetvalue(res, 0, 0));
                }

                if (res) {
                    PQclear(res);
                }
            }
            break;

        case PDO_ATTR_CONNECTION_STATUS:
            switch (PQstatus(H->server)) {
                case CONNECTION_STARTED:
                    ZVAL_STRINGL(return_value, "Waiting for connection to be made.", sizeof("Waiting for connection to be made.")-1);
                    break;

                case CONNECTION_MADE:
                case CONNECTION_OK:
                    ZVAL_STRINGL(return_value, "Connection OK; waiting to send.", sizeof("Connection OK; waiting to send.")-1);
                    break;

                case CONNECTION_AWAITING_RESPONSE:
                    ZVAL_STRINGL(return_value, "Waiting for a response from the server.", sizeof("Waiting for a response from the server.")-1);
                    break;

                case CONNECTION_AUTH_OK:
                    ZVAL_STRINGL(return_value, "Received authentication; waiting for backend start-up to finish.", sizeof("Received authentication; waiting for backend start-up to finish.")-1);
                    break;
#ifdef CONNECTION_SSL_STARTUP
                case CONNECTION_SSL_STARTUP:
                    ZVAL_STRINGL(return_value, "Negotiating SSL encryption.", sizeof("Negotiating SSL encryption.")-1);
                    break;
#endif
                case CONNECTION_SETENV:
                    ZVAL_STRINGL(return_value, "Negotiating environment-driven parameter settings.", sizeof("Negotiating environment-driven parameter settings.")-1);
                    break;

                case CONNECTION_BAD:
                default:
                    ZVAL_STRINGL(return_value, "Bad connection.", sizeof("Bad connection.")-1);
                    break;
            }
            break;

        case PDO_ATTR_SERVER_INFO: {
            int spid = PQbackendPID(H->server);


            zend_string *str_info =
                strpprintf(0,
                    "PID: %d; Client Encoding: %s; Is Superuser: %s; Session Authorization: %s; Date Style: %s",
                    spid,
                    (char*)PQparameterStatus(H->server, "client_encoding"),
                    (char*)PQparameterStatus(H->server, "is_superuser"),
                    (char*)PQparameterStatus(H->server, "session_authorization"),
                    (char*)PQparameterStatus(H->server, "DateStyle"));

            ZVAL_STR(return_value, str_info);
            break;
        }

        default:
            return 0;
    }

    return 1;
}

/* {{{ */
static int pdo_pgsql_check_liveness(pdo_dbh_t *dbh)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (!PQconsumeInput(H->server) || PQstatus(H->server) == CONNECTION_BAD) {
        PQreset(H->server);
    }
    return (PQstatus(H->server) == CONNECTION_OK) ? SUCCESS : FAILURE;
}
/* }}} */

static int pgsql_handle_in_transaction(pdo_dbh_t *dbh)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    return PQtransactionStatus(H->server) > PQTRANS_IDLE;
}

static int pdo_pgsql_transaction_cmd(const char *cmd, pdo_dbh_t *dbh)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    PGresult *res;
    int ret = 1;

    res = cat_pq_exec(H->server, cmd);

    if (PQresultStatus(res) != PGRES_COMMAND_OK) {
        pdo_pgsql_error(dbh, PQresultStatus(res), pdo_pgsql_sqlstate(res));
        ret = 0;
    }

    PQclear(res);
    return ret;
}

static int pgsql_handle_begin(pdo_dbh_t *dbh)
{
    return pdo_pgsql_transaction_cmd("BEGIN", dbh);
}

static int pgsql_handle_commit(pdo_dbh_t *dbh)
{
    int ret = pdo_pgsql_transaction_cmd("COMMIT", dbh);

    /* When deferred constraints are used the commit could
       fail, and a ROLLBACK implicitly ran. See bug #67462 */
    if (ret) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    } else {
        dbh->in_txn = pgsql_handle_in_transaction(dbh);
    }

    return ret;
}

static int pgsql_handle_rollback(pdo_dbh_t *dbh)
{
    int ret = pdo_pgsql_transaction_cmd("ROLLBACK", dbh);

    if (ret) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    }

    return ret;
}

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyFromArray)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    zval *pg_rows;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sa|sss!",
                    &table_name, &table_name_len, &pg_rows,
                    &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    if (!zend_hash_num_elements(Z_ARRVAL_P(pg_rows))) {
        zend_argument_value_error(2, "cannot be empty");
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }

    /* Obtain db Handle */
    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }
    pgsql_result = cat_pq_exec(H->server, query);

    efree(query);
    query = NULL;

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_IN && pgsql_result) {
        int command_failed = 0;
        size_t buffer_len = 0;
        zval *tmp;

        PQclear(pgsql_result);
        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pg_rows), tmp) {
            size_t query_len;
            if (!try_convert_to_string(tmp)) {
                efree(query);
                RETURN_THROWS();
            }

            if (buffer_len < Z_STRLEN_P(tmp)) {
                buffer_len = Z_STRLEN_P(tmp);
                query = erealloc(query, buffer_len + 2); /* room for \n\0 */
            }
            memcpy(query, Z_STRVAL_P(tmp), Z_STRLEN_P(tmp));
            query_len = Z_STRLEN_P(tmp);
            if (query[query_len - 1] != '\n') {
                query[query_len++] = '\n';
            }
            query[query_len] = '\0';
            if (PQputCopyData(H->server, query, query_len) != 1) {
                efree(query);
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
        } ZEND_HASH_FOREACH_END();
        if (query) {
            efree(query);
        }

        if (PQputCopyEnd(H->server, NULL) != 1) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            if (PGRES_COMMAND_OK != PQresultStatus(pgsql_result)) {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
                command_failed = 1;
            }
            PQclear(pgsql_result);
        }

        PDO_HANDLE_DBH_ERR();
        RETURN_BOOL(!command_failed);
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}
/* }}} */

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyFromFile)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *filename, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t  table_name_len, filename_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;
    PGresult *pgsql_result;
    ExecStatusType status;
    php_stream *stream;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sp|sss!",
                &table_name, &table_name_len, &filename, &filename_len,
                &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    /* Obtain db Handler */
    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    stream = php_stream_open_wrapper_ex(filename, "rb", 0, NULL, FG(default_context));
    if (!stream) {
        pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to open the file");
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }
    pgsql_result = cat_pq_exec(H->server, query);

    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_IN && pgsql_result) {
        char *buf;
        int command_failed = 0;
        size_t line_len = 0;

        PQclear(pgsql_result);
        while ((buf = php_stream_get_line(stream, NULL, 0, &line_len)) != NULL) {
            if (PQputCopyData(H->server, buf, line_len) != 1) {
                            efree(buf);
                            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                php_stream_close(stream);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
            efree(buf);
        }
        php_stream_close(stream);

        if (PQputCopyEnd(H->server, NULL) != 1) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            if (PGRES_COMMAND_OK != PQresultStatus(pgsql_result)) {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
                command_failed = 1;
            }
            PQclear(pgsql_result);
        }

        PDO_HANDLE_DBH_ERR();
        RETURN_BOOL(!command_failed);
    } else {
        php_stream_close(stream);
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}
/* }}} */


/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyToFile)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL, *filename = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len, filename_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    php_stream *stream;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sp|sss!",
                    &table_name, &table_name_len, &filename, &filename_len,
                    &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    stream = php_stream_open_wrapper_ex(filename, "wb", 0, NULL, FG(default_context));
    if (!stream) {
        pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to open the file for writing");
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }
    pgsql_result = cat_pq_exec(H->server, query);
    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_OUT && pgsql_result) {
        PQclear(pgsql_result);
        while (1) {
            char *csv = NULL;
            int ret = PQgetCopyData(H->server, &csv, 0);

            if (ret == -1) {
                break; /* done */
            } else if (ret > 0) {
                if (php_stream_write(stream, csv, ret) != (size_t)ret) {
                    pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to write to file");
                    PQfreemem(csv);
                    php_stream_close(stream);
                    PDO_HANDLE_DBH_ERR();
                    RETURN_FALSE;
                } else {
                    PQfreemem(csv);
                }
            } else {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                php_stream_close(stream);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
        }
        php_stream_close(stream);

        while ((pgsql_result = PQgetResult(H->server))) {
            PQclear(pgsql_result);
        }
        RETURN_TRUE;
    } else {
        php_stream_close(stream);
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}
/* }}} */

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyToArray)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|sss!",
        &table_name, &table_name_len,
        &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }
    pgsql_result = cat_pq_exec(H->server, query);
    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_OUT && pgsql_result) {
        PQclear(pgsql_result);
                array_init(return_value);

        while (1) {
            char *csv = NULL;
            int ret = PQgetCopyData(H->server, &csv, 0);
            if (ret == -1) {
                break; /* copy done */
            } else if (ret > 0) {
                add_next_index_stringl(return_value, csv, ret);
                PQfreemem(csv);
            } else {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            PQclear(pgsql_result);
        }
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}
/* }}} */


/* {{{ Creates a new large object, returning its identifier.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBCreate)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid lfd;

    ZEND_PARSE_PARAMETERS_NONE();

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;
    lfd = lo_creat(H->server, INV_READ|INV_WRITE);

    if (lfd != InvalidOid) {
        zend_string *buf = strpprintf(0, ZEND_ULONG_FMT, (zend_long) lfd);

        RETURN_STR(buf);
    }

    pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}
/* }}} */

/* {{{ Opens an existing large object stream.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBOpen)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid oid;
    int lfd;
    char *oidstr;
    size_t oidstrlen;
    char *modestr = (char *) "rb";
    size_t modestrlen;
    int mode = INV_READ;
    char *end_ptr;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "s|s",
                &oidstr, &oidstrlen, &modestr, &modestrlen)) {
        RETURN_THROWS();
    }

    oid = (Oid)strtoul(oidstr, &end_ptr, 10);
    if (oid == 0 && (errno == ERANGE || errno == EINVAL)) {
        RETURN_FALSE;
    }

    if (strpbrk(modestr, "+w")) {
        mode = INV_READ|INV_WRITE;
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    lfd = lo_open(H->server, oid, mode);

    if (lfd >= 0) {
        php_stream *stream = swow_pdo_pgsql_create_lob_stream(ZEND_THIS, lfd, oid);
        if (stream) {
            php_stream_to_zval(stream, return_value);
            return;
        }
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    }

    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}
/* }}} */

/* {{{ Deletes the large object identified by oid.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBUnlink)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid oid;
    char *oidstr, *end_ptr;
    size_t oidlen;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "s",
                &oidstr, &oidlen)) {
        RETURN_THROWS();
    }

    oid = (Oid)strtoul(oidstr, &end_ptr, 10);
    if (oid == 0 && (errno == ERANGE || errno == EINVAL)) {
        RETURN_FALSE;
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    if (1 == lo_unlink(H->server, oid)) {
        RETURN_TRUE;
    }

    pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}
/* }}} */

/* {{{ Get asynchronous notification */
PHP_METHOD(PDO_PGSql_Ext, pgsqlGetNotify)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    zend_long result_type = PDO_FETCH_USE_DEFAULT;
    zend_long ms_timeout = 0;
    PGnotify *pgsql_notify;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "|ll",
                &result_type, &ms_timeout)) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;

    if (result_type == PDO_FETCH_USE_DEFAULT) {
        result_type = dbh->default_fetch_type;
    }

    if (result_type != PDO_FETCH_BOTH && result_type != PDO_FETCH_ASSOC && result_type != PDO_FETCH_NUM) {
        zend_argument_value_error(1, "must be one of PDO::FETCH_BOTH, PDO::FETCH_ASSOC, or PDO::FETCH_NUM");
        RETURN_THROWS();
    }

    if (ms_timeout < 0) {
        zend_argument_value_error(2, "must be greater than or equal to 0");
        RETURN_THROWS();
#ifdef ZEND_ENABLE_ZVAL_LONG64
    } else if (ms_timeout > INT_MAX) {
        php_error_docref(NULL, E_WARNING, "Timeout was shrunk to %d", INT_MAX);
        ms_timeout = INT_MAX;
#endif
    }

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    if (!PQconsumeInput(H->server)) {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
    pgsql_notify = PQnotifies(H->server);

    if (ms_timeout && !pgsql_notify) {
        php_pollfd_for_ms(PQsocket(H->server), PHP_POLLREADABLE, (int)ms_timeout);

        if (!PQconsumeInput(H->server)) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }
        pgsql_notify = PQnotifies(H->server);
    }

    if (!pgsql_notify) {
        RETURN_FALSE;
    }

    array_init(return_value);
    if (result_type == PDO_FETCH_NUM || result_type == PDO_FETCH_BOTH) {
        add_index_string(return_value, 0, pgsql_notify->relname);
        add_index_long(return_value, 1, pgsql_notify->be_pid);
        if (pgsql_notify->extra && pgsql_notify->extra[0]) {
            add_index_string(return_value, 2, pgsql_notify->extra);
        }
    }
    if (result_type == PDO_FETCH_ASSOC || result_type == PDO_FETCH_BOTH) {
        add_assoc_string(return_value, "message", pgsql_notify->relname);
        add_assoc_long(return_value, "pid", pgsql_notify->be_pid);
        if (pgsql_notify->extra && pgsql_notify->extra[0]) {
            add_assoc_string(return_value, "payload", pgsql_notify->extra);
        }
    }

    PQfreemem(pgsql_notify);
}
/* }}} */

/* {{{ Get backend(server) pid */
PHP_METHOD(PDO_PGSql_Ext, pgsqlGetPid)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    ZEND_PARSE_PARAMETERS_NONE();

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    RETURN_LONG(PQbackendPID(H->server));
}
/* }}} */

static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, int kind)
{
    switch (kind) {
        case PDO_DBH_DRIVER_METHOD_KIND_DBH:
            return class_PDO_PGSql_Ext_methods;
        default:
            return NULL;
    }
}

static int pdo_pgsql_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
{
    bool bval = zval_get_long(val)? 1 : 0;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    switch (attr) {
        case PDO_ATTR_EMULATE_PREPARES:
            H->emulate_prepares = bval;
            return 1;
        case PDO_PGSQL_ATTR_DISABLE_PREPARES:
            H->disable_prepares = bval;
            return 1;
        default:
            return 0;
    }
}

static const struct pdo_dbh_methods pgsql_methods = {
    pgsql_handle_closer,
    pgsql_handle_preparer,
    pgsql_handle_doer,
    pgsql_handle_quoter,
    pgsql_handle_begin,
    pgsql_handle_commit,
    pgsql_handle_rollback,
    pdo_pgsql_set_attr,
    pdo_pgsql_last_insert_id,
    swow_pdo_pgsql_fetch_error_func,
    pdo_pgsql_get_attribute,
    pdo_pgsql_check_liveness,    /* check_liveness */
    pdo_pgsql_get_driver_methods,  /* get_driver_methods */
    NULL,
    pgsql_handle_in_transaction,
};

static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ */
{
    pdo_pgsql_db_handle *H;
    int ret = 0;
    char *conn_str, *p, *e;
    zend_string *tmp_user, *tmp_pass;
    zend_long connect_timeout = 30;

    H = pecalloc(1, sizeof(pdo_pgsql_db_handle), dbh->is_persistent);
    dbh->driver_data = H;

    dbh->skip_param_evt =
        1 << PDO_PARAM_EVT_EXEC_POST |
        1 << PDO_PARAM_EVT_FETCH_PRE |
        1 << PDO_PARAM_EVT_FETCH_POST;

    H->einfo.errcode = 0;
    H->einfo.errmsg = NULL;

    /* PostgreSQL wants params in the connect string to be separated by spaces,
     * if the PDO standard semicolons are used, we convert them to spaces
     */
    e = (char *) dbh->data_source + strlen(dbh->data_source);
    p = (char *) dbh->data_source;
    while ((p = memchr(p, ';', (e - p)))) {
        *p = ' ';
    }

    if (driver_options) {
        connect_timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, 30);
    }

    /* escape username and password, if provided */
    tmp_user = _pdo_pgsql_escape_credentials(dbh->username);
    tmp_pass = _pdo_pgsql_escape_credentials(dbh->password);

    /* support both full connection string & connection string + login and/or password */
    if (tmp_user && tmp_pass) {
        spprintf(&conn_str, 0, "%s user='%s' password='%s' connect_timeout=" ZEND_LONG_FMT, (char *) dbh->data_source, ZSTR_VAL(tmp_user), ZSTR_VAL(tmp_pass), connect_timeout);
    } else if (tmp_user) {
        spprintf(&conn_str, 0, "%s user='%s' connect_timeout=" ZEND_LONG_FMT, (char *) dbh->data_source, ZSTR_VAL(tmp_user), connect_timeout);
    } else if (tmp_pass) {
        spprintf(&conn_str, 0, "%s password='%s' connect_timeout=" ZEND_LONG_FMT, (char *) dbh->data_source, ZSTR_VAL(tmp_pass), connect_timeout);
    } else {
        spprintf(&conn_str, 0, "%s connect_timeout=" ZEND_LONG_FMT, (char *) dbh->data_source, connect_timeout);
    }

    H->server = cat_pq_connectdb(conn_str);
    H->lob_streams = (HashTable *) pemalloc(sizeof(HashTable), dbh->is_persistent);
    zend_hash_init(H->lob_streams, 0, NULL, NULL, 1);

    if (tmp_user) {
        zend_string_release_ex(tmp_user, 0);
    }
    if (tmp_pass) {
        zend_string_release_ex(tmp_pass, 0);
    }

    efree(conn_str);

    if (PQstatus(H->server) != CONNECTION_OK) {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, PHP_PDO_PGSQL_CONNECTION_FAILURE_SQLSTATE);
        goto cleanup;
    }

    PQsetNoticeProcessor(H->server, (void(*)(void*,const char*))_swow_pdo_pgsql_notice, (void *)&dbh);

    H->attached = 1;
    H->pgoid = -1;

    dbh->methods = &pgsql_methods;
    dbh->alloc_own_columns = 1;
    dbh->max_escaped_char_length = 2;

    ret = 1;

cleanup:
    dbh->methods = &pgsql_methods;
    if (!ret) {
        pgsql_handle_closer(dbh);
    }

    return ret;
}
/* }}} */

/* Git hash: php/php-src@22c9e7e27ea4396f43c4496cce6c058937976e90 */
#elif PHP_VERSION_ID >= 80100

static bool pgsql_handle_in_transaction(pdo_dbh_t *dbh);

static char * _pdo_pgsql_trim_message(const char *message, int persistent)
{
    size_t i = strlen(message)-1;
    char *tmp;

    if (i>1 && (message[i-1] == '\r' || message[i-1] == '\n') && message[i] == '.') {
        --i;
    }
    while (i>0 && (message[i] == '\r' || message[i] == '\n')) {
        --i;
    }
    ++i;
    tmp = pemalloc(i + 1, persistent);
    memcpy(tmp, message, i);
    tmp[i] = '\0';

    return tmp;
}

static zend_string* _pdo_pgsql_escape_credentials(char *str)
{
    if (str) {
        return php_addcslashes_str(str, strlen(str), "\\'", sizeof("\\'"));
    }

    return NULL;
}

int _swow_pdo_pgsql_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, int errcode, const char *sqlstate, const char *msg, const char *file, int line) /* {{{ */
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_error_type *pdo_err = stmt ? &stmt->error_code : &dbh->error_code;
    pdo_pgsql_error_info *einfo = &H->einfo;
    char *errmsg = PQerrorMessage(H->server);

    einfo->errcode = errcode;
    einfo->file = file;
    einfo->line = line;

    if (einfo->errmsg) {
        pefree(einfo->errmsg, dbh->is_persistent);
        einfo->errmsg = NULL;
    }

    if (sqlstate == NULL || strlen(sqlstate) >= sizeof(pdo_error_type)) {
        strcpy(*pdo_err, "HY000");
    }
    else {
        strcpy(*pdo_err, sqlstate);
    }

    if (msg) {
        einfo->errmsg = pestrdup(msg, dbh->is_persistent);
    }
    else if (errmsg) {
        einfo->errmsg = _pdo_pgsql_trim_message(errmsg, dbh->is_persistent);
    }

    if (!dbh->methods) {
        pdo_throw_exception(einfo->errcode, einfo->errmsg, pdo_err);
    }

    return errcode;
}
/* }}} */

static void _swow_pdo_pgsql_notice(pdo_dbh_t *dbh, const char *message) /* {{{ */
{
/*    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; */
}
/* }}} */

static void swow_pdo_pgsql_fetch_error_func(pdo_dbh_t *dbh, pdo_stmt_t *stmt, zval *info) /* {{{ */
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_pgsql_error_info *einfo = &H->einfo;

    if (einfo->errcode) {
        add_next_index_long(info, einfo->errcode);
    } else {
        /* Add null to respect expected info array structure */
        add_next_index_null(info);
    }
    if (einfo->errmsg) {
        add_next_index_string(info, einfo->errmsg);
    }
}
/* }}} */

/* {{{ pdo_pgsql_create_lob_stream */
static ssize_t swow_pgsql_lob_write(php_stream *stream, const char *buf, size_t count)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    return lo_write(self->conn, self->lfd, (char*)buf, count);
}

static ssize_t swow_pgsql_lob_read(php_stream *stream, char *buf, size_t count)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    return lo_read(self->conn, self->lfd, buf, count);
}

static int swow_pgsql_lob_close(php_stream *stream, int close_handle)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)(Z_PDO_DBH_P(&self->dbh))->driver_data;

    if (close_handle) {
        lo_close(self->conn, self->lfd);
    }
    zend_hash_index_del(H->lob_streams, php_stream_get_resource_id(stream));
    zval_ptr_dtor(&self->dbh);
    efree(self);
    return 0;
}

static int swow_pgsql_lob_flush(php_stream *stream)
{
    return 0;
}

static int swow_pgsql_lob_seek(php_stream *stream, zend_off_t offset, int whence,
        zend_off_t *newoffset)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
#if defined(HAVE_PG_LO64) && defined(ZEND_ENABLE_ZVAL_LONG64)
    zend_off_t pos = lo_lseek64(self->conn, self->lfd, offset, whence);
#else
    zend_off_t pos = lo_lseek(self->conn, self->lfd, offset, whence);
#endif
    *newoffset = pos;
    return pos >= 0 ? 0 : -1;
}

const php_stream_ops swow_pdo_pgsql_lob_stream_ops = {
    swow_pgsql_lob_write,
    swow_pgsql_lob_read,
    swow_pgsql_lob_close,
    swow_pgsql_lob_flush,
    "pdo_pgsql lob stream",
    swow_pgsql_lob_seek,
    NULL,
    NULL,
    NULL
};

php_stream *swow_pdo_pgsql_create_lob_stream(zval *dbh, int lfd, Oid oid)
{
    php_stream *stm;
    struct pdo_pgsql_lob_self *self = ecalloc(1, sizeof(*self));
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)(Z_PDO_DBH_P(dbh))->driver_data;

    ZVAL_COPY_VALUE(&self->dbh, dbh);
    self->lfd = lfd;
    self->oid = oid;
    self->conn = H->server;

    stm = php_stream_alloc(&swow_pdo_pgsql_lob_stream_ops, self, 0, "r+b");

    if (stm) {
        Z_ADDREF_P(dbh);
        zend_hash_index_add_ptr(H->lob_streams, php_stream_get_resource_id(stm), stm->res);
        return stm;
    }

    efree(self);
    return NULL;
}
/* }}} */

void swow_pdo_pgsql_close_lob_streams(pdo_dbh_t *dbh)
{
    zend_resource *res;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (H->lob_streams) {
        ZEND_HASH_REVERSE_FOREACH_PTR(H->lob_streams, res) {
            if (res->type >= 0) {
                zend_list_close(res);
            }
        } ZEND_HASH_FOREACH_END();
    }
}

static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (H) {
        if (H->lob_streams) {
            swow_pdo_pgsql_close_lob_streams(dbh);
            zend_hash_destroy(H->lob_streams);
            pefree(H->lob_streams, dbh->is_persistent);
            H->lob_streams = NULL;
        }
        if (H->server) {
            PQfinish(H->server);
            H->server = NULL;
        }
        if (H->einfo.errmsg) {
            pefree(H->einfo.errmsg, dbh->is_persistent);
            H->einfo.errmsg = NULL;
        }
        pefree(H, dbh->is_persistent);
        dbh->driver_data = NULL;
    }
}
/* }}} */

static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_pgsql_stmt *S = ecalloc(1, sizeof(pdo_pgsql_stmt));
    int scrollable;
    int ret;
    zend_string *nsql = NULL;
    int emulate = 0;
    int execute_only = 0;

    S->H = H;
    stmt->driver_data = S;
    stmt->methods = &swow_pgsql_stmt_methods;

    scrollable = pdo_attr_lval(driver_options, PDO_ATTR_CURSOR,
        PDO_CURSOR_FWDONLY) == PDO_CURSOR_SCROLL;

    if (scrollable) {
        if (S->cursor_name) {
            efree(S->cursor_name);
        }
        spprintf(&S->cursor_name, 0, "pdo_crsr_%08x", ++H->stmt_counter);
        emulate = 1;
    } else if (driver_options) {
        if (pdo_attr_lval(driver_options, PDO_ATTR_EMULATE_PREPARES, H->emulate_prepares) == 1) {
            emulate = 1;
        }
        if (pdo_attr_lval(driver_options, PDO_PGSQL_ATTR_DISABLE_PREPARES, H->disable_prepares) == 1) {
            execute_only = 1;
        }
    } else {
        emulate = H->disable_native_prepares || H->emulate_prepares;
        execute_only = H->disable_prepares;
    }

    if (!emulate && PQprotocolVersion(H->server) <= 2) {
        emulate = 1;
    }

    if (emulate) {
        stmt->supports_placeholders = PDO_PLACEHOLDER_NONE;
    } else {
        stmt->supports_placeholders = PDO_PLACEHOLDER_NAMED;
        stmt->named_rewrite_template = "$%d";
    }

    ret = pdo_parse_params(stmt, sql, &nsql);

    if (ret == -1) {
        /* couldn't grok it */
        strcpy(dbh->error_code, stmt->error_code);
        return false;
    } else if (ret == 1) {
        /* query was re-written */
        S->query = nsql;
    } else {
        S->query = zend_string_copy(sql);
    }

    if (!emulate && !execute_only) {
        /* prepared query: set the query name and defer the
           actual prepare until the first execute call */
        spprintf(&S->stmt_name, 0, "pdo_stmt_%08x", ++H->stmt_counter);
    }

    return true;
}

static zend_long pgsql_handle_doer(pdo_dbh_t *dbh, const zend_string *sql)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    PGresult *res;
    zend_long ret = 1;
    ExecStatusType qs;

    bool in_trans = pgsql_handle_in_transaction(dbh);

    if (!(res = cat_pq_exec(H->server, ZSTR_VAL(sql)))) {
        /* fatal error */
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
        return -1;
    }
    qs = PQresultStatus(res);
    if (qs != PGRES_COMMAND_OK && qs != PGRES_TUPLES_OK) {
        pdo_pgsql_error(dbh, qs, pdo_pgsql_sqlstate(res));
        PQclear(res);
        return -1;
    }
    H->pgoid = PQoidValue(res);
    if (qs == PGRES_COMMAND_OK) {
        ret = ZEND_ATOL(PQcmdTuples(res));
    } else {
        ret = Z_L(0);
    }
    PQclear(res);
    if (in_trans && !pgsql_handle_in_transaction(dbh)) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    }

    return ret;
}

static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquoted, enum pdo_param_type paramtype)
{
    unsigned char *escaped;
    char *quoted;
    size_t quotedlen;
    zend_string *quoted_str;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    size_t tmp_len;

    switch (paramtype) {
        case PDO_PARAM_LOB:
            /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */
            escaped = PQescapeByteaConn(H->server, (unsigned char *)ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &tmp_len);
            quotedlen = tmp_len + 1;
            quoted = emalloc(quotedlen + 1);
            memcpy(quoted+1, escaped, quotedlen-2);
            quoted[0] = '\'';
            quoted[quotedlen-1] = '\'';
            quoted[quotedlen] = '\0';
            PQfreemem(escaped);
            break;
        default:
            quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3);
            quoted[0] = '\'';
            quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), NULL);
            quoted[quotedlen + 1] = '\'';
            quoted[quotedlen + 2] = '\0';
            quotedlen += 2;
    }

    quoted_str = zend_string_init(quoted, quotedlen, 0);
    efree(quoted);
    return quoted_str;
}

static zend_string *pdo_pgsql_last_insert_id(pdo_dbh_t *dbh, const zend_string *name)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    zend_string *id = NULL;
    PGresult *res;
    ExecStatusType status;

    if (name == NULL) {
        res = cat_pq_exec(H->server, "SELECT LASTVAL()");
    } else {
        const char *q[1];
        q[0] = ZSTR_VAL(name);

        res = cat_pq_exec_params(H->server, "SELECT CURRVAL($1)", 1, NULL, q, NULL, NULL, 0);
    }
    status = PQresultStatus(res);

    if (res && (status == PGRES_TUPLES_OK)) {
        id = zend_string_init((char *)PQgetvalue(res, 0, 0), PQgetlength(res, 0, 0), 0);
    } else {
        pdo_pgsql_error(dbh, status, pdo_pgsql_sqlstate(res));
    }

    if (res) {
        PQclear(res);
    }

    return id;
}

void swow_pdo_libpq_version(char *buf, size_t len)
{
    int version = PQlibVersion();
    int major = version / 10000;
    if (major >= 10) {
        int minor = version % 10000;
        snprintf(buf, len, "%d.%d", major, minor);
    } else {
        int minor = version / 100 % 100;
        int revision = version % 100;
        snprintf(buf, len, "%d.%d.%d", major, minor, revision);
    }
}

static int pdo_pgsql_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_value)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    switch (attr) {
        case PDO_ATTR_EMULATE_PREPARES:
            ZVAL_BOOL(return_value, H->emulate_prepares);
            break;

        case PDO_PGSQL_ATTR_DISABLE_PREPARES:
            ZVAL_BOOL(return_value, H->disable_prepares);
            break;

        case PDO_ATTR_CLIENT_VERSION: {
            char buf[16];
            swow_pdo_libpq_version(buf, sizeof(buf));
            ZVAL_STRING(return_value, buf);
            break;
        }

        case PDO_ATTR_SERVER_VERSION:
            if (PQprotocolVersion(H->server) >= 3) { /* PostgreSQL 7.4 or later */
                ZVAL_STRING(return_value, (char*)PQparameterStatus(H->server, "server_version"));
            } else /* emulate above via a query */
            {
                PGresult *res = cat_pq_exec(H->server, "SELECT VERSION()");
                if (res && PQresultStatus(res) == PGRES_TUPLES_OK) {
                    ZVAL_STRING(return_value, (char *)PQgetvalue(res, 0, 0));
                }

                if (res) {
                    PQclear(res);
                }
            }
            break;

        case PDO_ATTR_CONNECTION_STATUS:
            switch (PQstatus(H->server)) {
                case CONNECTION_STARTED:
                    ZVAL_STRINGL(return_value, "Waiting for connection to be made.", sizeof("Waiting for connection to be made.")-1);
                    break;

                case CONNECTION_MADE:
                case CONNECTION_OK:
                    ZVAL_STRINGL(return_value, "Connection OK; waiting to send.", sizeof("Connection OK; waiting to send.")-1);
                    break;

                case CONNECTION_AWAITING_RESPONSE:
                    ZVAL_STRINGL(return_value, "Waiting for a response from the server.", sizeof("Waiting for a response from the server.")-1);
                    break;

                case CONNECTION_AUTH_OK:
                    ZVAL_STRINGL(return_value, "Received authentication; waiting for backend start-up to finish.", sizeof("Received authentication; waiting for backend start-up to finish.")-1);
                    break;
#ifdef CONNECTION_SSL_STARTUP
                case CONNECTION_SSL_STARTUP:
                    ZVAL_STRINGL(return_value, "Negotiating SSL encryption.", sizeof("Negotiating SSL encryption.")-1);
                    break;
#endif
                case CONNECTION_SETENV:
                    ZVAL_STRINGL(return_value, "Negotiating environment-driven parameter settings.", sizeof("Negotiating environment-driven parameter settings.")-1);
                    break;

                case CONNECTION_BAD:
                default:
                    ZVAL_STRINGL(return_value, "Bad connection.", sizeof("Bad connection.")-1);
                    break;
            }
            break;

        case PDO_ATTR_SERVER_INFO: {
            int spid = PQbackendPID(H->server);


            zend_string *str_info =
                strpprintf(0,
                    "PID: %d; Client Encoding: %s; Is Superuser: %s; Session Authorization: %s; Date Style: %s",
                    spid,
                    (char*)PQparameterStatus(H->server, "client_encoding"),
                    (char*)PQparameterStatus(H->server, "is_superuser"),
                    (char*)PQparameterStatus(H->server, "session_authorization"),
                    (char*)PQparameterStatus(H->server, "DateStyle"));

            ZVAL_STR(return_value, str_info);
            break;
        }

        default:
            return 0;
    }

    return 1;
}

/* {{{ */
static zend_result pdo_pgsql_check_liveness(pdo_dbh_t *dbh)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (!PQconsumeInput(H->server) || PQstatus(H->server) == CONNECTION_BAD) {
        PQreset(H->server);
    }
    return (PQstatus(H->server) == CONNECTION_OK) ? SUCCESS : FAILURE;
}
/* }}} */

static bool pgsql_handle_in_transaction(pdo_dbh_t *dbh)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    return PQtransactionStatus(H->server) > PQTRANS_IDLE;
}

static bool pdo_pgsql_transaction_cmd(const char *cmd, pdo_dbh_t *dbh)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    PGresult *res;
    bool ret = true;

    res = cat_pq_exec(H->server, cmd);

    if (PQresultStatus(res) != PGRES_COMMAND_OK) {
        pdo_pgsql_error(dbh, PQresultStatus(res), pdo_pgsql_sqlstate(res));
        ret = false;
    }

    PQclear(res);
    return ret;
}

static bool pgsql_handle_begin(pdo_dbh_t *dbh)
{
    return pdo_pgsql_transaction_cmd("BEGIN", dbh);
}

static bool pgsql_handle_commit(pdo_dbh_t *dbh)
{
    bool ret = pdo_pgsql_transaction_cmd("COMMIT", dbh);

    /* When deferred constraints are used the commit could
       fail, and a ROLLBACK implicitly ran. See bug #67462 */
    if (ret) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    } else {
        dbh->in_txn = pgsql_handle_in_transaction(dbh);
    }

    return ret;
}

static bool pgsql_handle_rollback(pdo_dbh_t *dbh)
{
    int ret = pdo_pgsql_transaction_cmd("ROLLBACK", dbh);

    if (ret) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    }

    return ret;
}

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyFromArray)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    zval *pg_rows;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sa|sss!",
                    &table_name, &table_name_len, &pg_rows,
                    &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    if (!zend_hash_num_elements(Z_ARRVAL_P(pg_rows))) {
        zend_argument_value_error(2, "cannot be empty");
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }

    /* Obtain db Handle */
    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }
    pgsql_result = cat_pq_exec(H->server, query);

    efree(query);
    query = NULL;

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_IN && pgsql_result) {
        int command_failed = 0;
        size_t buffer_len = 0;
        zval *tmp;

        PQclear(pgsql_result);
        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pg_rows), tmp) {
            size_t query_len;
            if (!try_convert_to_string(tmp)) {
                efree(query);
                RETURN_THROWS();
            }

            if (buffer_len < Z_STRLEN_P(tmp)) {
                buffer_len = Z_STRLEN_P(tmp);
                query = erealloc(query, buffer_len + 2); /* room for \n\0 */
            }
            memcpy(query, Z_STRVAL_P(tmp), Z_STRLEN_P(tmp));
            query_len = Z_STRLEN_P(tmp);
            if (query[query_len - 1] != '\n') {
                query[query_len++] = '\n';
            }
            query[query_len] = '\0';
            if (PQputCopyData(H->server, query, query_len) != 1) {
                efree(query);
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
        } ZEND_HASH_FOREACH_END();
        if (query) {
            efree(query);
        }

        if (PQputCopyEnd(H->server, NULL) != 1) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            if (PGRES_COMMAND_OK != PQresultStatus(pgsql_result)) {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
                command_failed = 1;
            }
            PQclear(pgsql_result);
        }

        PDO_HANDLE_DBH_ERR();
        RETURN_BOOL(!command_failed);
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}
/* }}} */

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyFromFile)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *filename, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t  table_name_len, filename_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;
    PGresult *pgsql_result;
    ExecStatusType status;
    php_stream *stream;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sp|sss!",
                &table_name, &table_name_len, &filename, &filename_len,
                &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    /* Obtain db Handler */
    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    stream = php_stream_open_wrapper_ex(filename, "rb", 0, NULL, FG(default_context));
    if (!stream) {
        pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to open the file");
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }
    pgsql_result = cat_pq_exec(H->server, query);

    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_IN && pgsql_result) {
        char *buf;
        int command_failed = 0;
        size_t line_len = 0;

        PQclear(pgsql_result);
        while ((buf = php_stream_get_line(stream, NULL, 0, &line_len)) != NULL) {
            if (PQputCopyData(H->server, buf, line_len) != 1) {
                efree(buf);
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                php_stream_close(stream);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
            efree(buf);
        }
        php_stream_close(stream);

        if (PQputCopyEnd(H->server, NULL) != 1) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            if (PGRES_COMMAND_OK != PQresultStatus(pgsql_result)) {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
                command_failed = 1;
            }
            PQclear(pgsql_result);
        }

        PDO_HANDLE_DBH_ERR();
        RETURN_BOOL(!command_failed);
    } else {
        php_stream_close(stream);
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}
/* }}} */


/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyToFile)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL, *filename = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len, filename_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    php_stream *stream;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sp|sss!",
                    &table_name, &table_name_len, &filename, &filename_len,
                    &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    stream = php_stream_open_wrapper_ex(filename, "wb", 0, NULL, FG(default_context));
    if (!stream) {
        pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to open the file for writing");
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }
    pgsql_result = cat_pq_exec(H->server, query);
    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_OUT && pgsql_result) {
        PQclear(pgsql_result);
        while (1) {
            char *csv = NULL;
            int ret = PQgetCopyData(H->server, &csv, 0);

            if (ret == -1) {
                break; /* done */
            } else if (ret > 0) {
                if (php_stream_write(stream, csv, ret) != (size_t)ret) {
                    pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to write to file");
                    PQfreemem(csv);
                    php_stream_close(stream);
                    PDO_HANDLE_DBH_ERR();
                    RETURN_FALSE;
                } else {
                    PQfreemem(csv);
                }
            } else {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                php_stream_close(stream);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
        }
        php_stream_close(stream);

        while ((pgsql_result = PQgetResult(H->server))) {
            PQclear(pgsql_result);
        }
        RETURN_TRUE;
    } else {
        php_stream_close(stream);
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}
/* }}} */

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyToArray)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|sss!",
        &table_name, &table_name_len,
        &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }
    pgsql_result = cat_pq_exec(H->server, query);
    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_OUT && pgsql_result) {
        PQclear(pgsql_result);
                array_init(return_value);

        while (1) {
            char *csv = NULL;
            int ret = PQgetCopyData(H->server, &csv, 0);
            if (ret == -1) {
                break; /* copy done */
            } else if (ret > 0) {
                add_next_index_stringl(return_value, csv, ret);
                PQfreemem(csv);
            } else {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            PQclear(pgsql_result);
        }
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}
/* }}} */


/* {{{ Creates a new large object, returning its identifier.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBCreate)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid lfd;

    ZEND_PARSE_PARAMETERS_NONE();

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;
    lfd = lo_creat(H->server, INV_READ|INV_WRITE);

    if (lfd != InvalidOid) {
        zend_string *buf = strpprintf(0, ZEND_ULONG_FMT, (zend_long) lfd);

        RETURN_STR(buf);
    }

    pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}
/* }}} */

/* {{{ Opens an existing large object stream.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBOpen)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid oid;
    int lfd;
    char *oidstr;
    size_t oidstrlen;
    char *modestr = (char *) "rb";
    size_t modestrlen;
    int mode = INV_READ;
    char *end_ptr;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "s|s",
                &oidstr, &oidstrlen, &modestr, &modestrlen)) {
        RETURN_THROWS();
    }

    oid = (Oid)strtoul(oidstr, &end_ptr, 10);
    if (oid == 0 && (errno == ERANGE || errno == EINVAL)) {
        RETURN_FALSE;
    }

    if (strpbrk(modestr, "+w")) {
        mode = INV_READ|INV_WRITE;
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    lfd = lo_open(H->server, oid, mode);

    if (lfd >= 0) {
        php_stream *stream = swow_pdo_pgsql_create_lob_stream(ZEND_THIS, lfd, oid);
        if (stream) {
            php_stream_to_zval(stream, return_value);
            return;
        }
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    }

    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}
/* }}} */

/* {{{ Deletes the large object identified by oid.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBUnlink)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid oid;
    char *oidstr, *end_ptr;
    size_t oidlen;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "s",
                &oidstr, &oidlen)) {
        RETURN_THROWS();
    }

    oid = (Oid)strtoul(oidstr, &end_ptr, 10);
    if (oid == 0 && (errno == ERANGE || errno == EINVAL)) {
        RETURN_FALSE;
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    if (1 == lo_unlink(H->server, oid)) {
        RETURN_TRUE;
    }

    pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}
/* }}} */

/* {{{ Get asynchronous notification */
PHP_METHOD(PDO_PGSql_Ext, pgsqlGetNotify)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    zend_long result_type = PDO_FETCH_USE_DEFAULT;
    zend_long ms_timeout = 0;
    PGnotify *pgsql_notify;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "|ll",
                &result_type, &ms_timeout)) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;

    if (result_type == PDO_FETCH_USE_DEFAULT) {
        result_type = dbh->default_fetch_type;
    }

    if (result_type != PDO_FETCH_BOTH && result_type != PDO_FETCH_ASSOC && result_type != PDO_FETCH_NUM) {
        zend_argument_value_error(1, "must be one of PDO::FETCH_BOTH, PDO::FETCH_ASSOC, or PDO::FETCH_NUM");
        RETURN_THROWS();
    }

    if (ms_timeout < 0) {
        zend_argument_value_error(2, "must be greater than or equal to 0");
        RETURN_THROWS();
#ifdef ZEND_ENABLE_ZVAL_LONG64
    } else if (ms_timeout > INT_MAX) {
        php_error_docref(NULL, E_WARNING, "Timeout was shrunk to %d", INT_MAX);
        ms_timeout = INT_MAX;
#endif
    }

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    if (!PQconsumeInput(H->server)) {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
    pgsql_notify = PQnotifies(H->server);

    if (ms_timeout && !pgsql_notify) {
        php_pollfd_for_ms(PQsocket(H->server), PHP_POLLREADABLE, (int)ms_timeout);

        if (!PQconsumeInput(H->server)) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }
        pgsql_notify = PQnotifies(H->server);
    }

    if (!pgsql_notify) {
        RETURN_FALSE;
    }

    array_init(return_value);
    if (result_type == PDO_FETCH_NUM || result_type == PDO_FETCH_BOTH) {
        add_index_string(return_value, 0, pgsql_notify->relname);
        add_index_long(return_value, 1, pgsql_notify->be_pid);
        if (pgsql_notify->extra && pgsql_notify->extra[0]) {
            add_index_string(return_value, 2, pgsql_notify->extra);
        }
    }
    if (result_type == PDO_FETCH_ASSOC || result_type == PDO_FETCH_BOTH) {
        add_assoc_string(return_value, "message", pgsql_notify->relname);
        add_assoc_long(return_value, "pid", pgsql_notify->be_pid);
        if (pgsql_notify->extra && pgsql_notify->extra[0]) {
            add_assoc_string(return_value, "payload", pgsql_notify->extra);
        }
    }

    PQfreemem(pgsql_notify);
}
/* }}} */

/* {{{ Get backend(server) pid */
PHP_METHOD(PDO_PGSql_Ext, pgsqlGetPid)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    ZEND_PARSE_PARAMETERS_NONE();

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    RETURN_LONG(PQbackendPID(H->server));
}
/* }}} */

static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, int kind)
{
    switch (kind) {
        case PDO_DBH_DRIVER_METHOD_KIND_DBH:
            return class_PDO_PGSql_Ext_methods;
        default:
            return NULL;
    }
}

static bool pdo_pgsql_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
{
    bool bval;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    switch (attr) {
        case PDO_ATTR_EMULATE_PREPARES:
            if (!pdo_get_bool_param(&bval, val)) {
                return false;
            }
            H->emulate_prepares = bval;
            return true;
        case PDO_PGSQL_ATTR_DISABLE_PREPARES:
            if (!pdo_get_bool_param(&bval, val)) {
                return false;
            }
            H->disable_prepares = bval;
            return true;
        default:
            return false;
    }
}

static const struct pdo_dbh_methods pgsql_methods = {
    pgsql_handle_closer,
    pgsql_handle_preparer,
    pgsql_handle_doer,
    pgsql_handle_quoter,
    pgsql_handle_begin,
    pgsql_handle_commit,
    pgsql_handle_rollback,
    pdo_pgsql_set_attr,
    pdo_pgsql_last_insert_id,
    swow_pdo_pgsql_fetch_error_func,
    pdo_pgsql_get_attribute,
    pdo_pgsql_check_liveness,    /* check_liveness */
    pdo_pgsql_get_driver_methods,  /* get_driver_methods */
    NULL,
    pgsql_handle_in_transaction,
    NULL /* get_gc */
};

static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ */
{
    pdo_pgsql_db_handle *H;
    int ret = 0;
    char *conn_str, *p, *e;
    zend_string *tmp_user, *tmp_pass;
    zend_long connect_timeout = 30;

    H = pecalloc(1, sizeof(pdo_pgsql_db_handle), dbh->is_persistent);
    dbh->driver_data = H;

    dbh->skip_param_evt =
        1 << PDO_PARAM_EVT_EXEC_POST |
        1 << PDO_PARAM_EVT_FETCH_PRE |
        1 << PDO_PARAM_EVT_FETCH_POST;

    H->einfo.errcode = 0;
    H->einfo.errmsg = NULL;

    /* PostgreSQL wants params in the connect string to be separated by spaces,
     * if the PDO standard semicolons are used, we convert them to spaces
     */
    e = (char *) dbh->data_source + strlen(dbh->data_source);
    p = (char *) dbh->data_source;
    while ((p = memchr(p, ';', (e - p)))) {
        *p = ' ';
    }

    if (driver_options) {
        connect_timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, 30);
    }

    /* escape username and password, if provided */
    tmp_user = _pdo_pgsql_escape_credentials(dbh->username);
    tmp_pass = _pdo_pgsql_escape_credentials(dbh->password);

    /* support both full connection string & connection string + login and/or password */
    if (tmp_user && tmp_pass) {
        spprintf(&conn_str, 0, "%s user='%s' password='%s' connect_timeout=" ZEND_LONG_FMT, (char *) dbh->data_source, ZSTR_VAL(tmp_user), ZSTR_VAL(tmp_pass), connect_timeout);
    } else if (tmp_user) {
        spprintf(&conn_str, 0, "%s user='%s' connect_timeout=" ZEND_LONG_FMT, (char *) dbh->data_source, ZSTR_VAL(tmp_user), connect_timeout);
    } else if (tmp_pass) {
        spprintf(&conn_str, 0, "%s password='%s' connect_timeout=" ZEND_LONG_FMT, (char *) dbh->data_source, ZSTR_VAL(tmp_pass), connect_timeout);
    } else {
        spprintf(&conn_str, 0, "%s connect_timeout=" ZEND_LONG_FMT, (char *) dbh->data_source, connect_timeout);
    }

    H->server = cat_pq_connectdb(conn_str);
    H->lob_streams = (HashTable *) pemalloc(sizeof(HashTable), dbh->is_persistent);
    zend_hash_init(H->lob_streams, 0, NULL, NULL, 1);

    if (tmp_user) {
        zend_string_release_ex(tmp_user, 0);
    }
    if (tmp_pass) {
        zend_string_release_ex(tmp_pass, 0);
    }

    efree(conn_str);

    if (PQstatus(H->server) != CONNECTION_OK) {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, PHP_PDO_PGSQL_CONNECTION_FAILURE_SQLSTATE);
        goto cleanup;
    }

    PQsetNoticeProcessor(H->server, (void(*)(void*,const char*))_swow_pdo_pgsql_notice, (void *)&dbh);

    H->attached = 1;
    H->pgoid = -1;

    dbh->methods = &pgsql_methods;
    dbh->alloc_own_columns = 1;
    dbh->max_escaped_char_length = 2;

    ret = 1;

cleanup:
    dbh->methods = &pgsql_methods;
    if (!ret) {
        pgsql_handle_closer(dbh);
    }

    return ret;
}
/* }}} */

#endif

const pdo_driver_t swow_pdo_pgsql_driver = {
    PDO_DRIVER_HEADER(pgsql),
    pdo_pgsql_handle_factory
};

#include "swow.h"

int swow_libpq_version = 0;
cat_bool_t swow_pgsql_hooked = cat_false;

zend_result swow_pgsql_module_init(INIT_FUNC_ARGS)
{
    if (!zend_hash_str_exists(&module_registry, ZEND_STRL("pdo"))) {
        php_error_docref(NULL, E_WARNING, "Swow pdo_pgsql hook not enabled, pdo extension not enabled");
        return SUCCESS;
    }

#ifdef COMPILE_DL_SWOW
    const char *library_paths[] =
# if defined(CAT_OS_DARWIN)
#  define LIBPQ_SO_NAME "libpq.5.dylib"
        {
            // macports/brew
            "/opt/local/lib/",
            // brew
            "/usr/local/Cellar/libpq@15/lib/",
            "/usr/local/Cellar/libpq@14/lib/",
            // brew (new)
            // @see: https://earthly.dev/blog/homebrew-on-m1/)
            "/opt/homebrew/opt/libpq/lib/",
            // postgres.app
            "/Applications/Postgres.app/Contents/Versions/latest/lib/",
            // fink
            "/sw/lib/",
        }
# elif defined(CAT_OS_WIN)
#  define LIBPQ_SO_NAME "libpq.dll"
        {
            "C:\\Program Files\\PostgreSQL\\15\\bin\\",
            "C:\\Program Files\\PostgreSQL\\14\\bin\\",
            "C:\\Program Files (x86)\\PostgreSQL\\15\\bin\\",
            "C:\\Program Files (x86)\\PostgreSQL\\14\\bin\\",
        }
# else
#  define LIBPQ_SO_NAME "libpq." PHP_SHLIB_SUFFIX
        { "" }
# endif
    ;

    DL_HANDLE dummy_handle = DL_LOAD(LIBPQ_SO_NAME);
    if (!dummy_handle) {
        char name_buf[64];
        for (int i = 0; i < CAT_ARRAY_SIZE(library_paths); i++) {
            snprintf(name_buf, sizeof(name_buf), "%s%s", library_paths[i], LIBPQ_SO_NAME);

#ifdef CAT_OS_WIN
            SetDllDirectoryA(library_paths[i]);
#endif
            dummy_handle = DL_LOAD(name_buf);
#ifdef CAT_OS_WIN
            SetDllDirectoryA(NULL);
#endif
            if (dummy_handle) {
                break;
            }
        }

    }
    if (!dummy_handle) {
        smart_str paths = {0};
        for (int i = 0; i < CAT_ARRAY_SIZE(library_paths); i++) {
            smart_str_appends(&paths, library_paths[i]);
            smart_str_appendc(&paths, ',');
        }
        ZSTR_LEN(paths.s) -= 1;
        smart_str_0(&paths);
        php_error_docref(NULL, E_WARNING, "Swow pdo_pgsql hook not enabled, " LIBPQ_SO_NAME " not found, (search paths: %s)", ZSTR_VAL(paths.s));
        smart_str_free(&paths);
        return SUCCESS;
    }
#endif // COMPILE_DL_SWOW

    swow_libpq_version = PQlibVersion();
    if (swow_libpq_version < 90100) {
        php_error_docref(NULL, E_WARNING, "Swow pdo_pgsql hook not enabled, libpq is too old (%d < 90100)", swow_libpq_version);
        return SUCCESS;
    }

    // php_pdo_register_driver will unregister swow_pdo_pgsql_driver->driver_name i.e. "pdo_pgsql"
    php_pdo_unregister_driver(&swow_pdo_pgsql_driver);
    if (php_pdo_register_driver(&swow_pdo_pgsql_driver) == SUCCESS) {
        swow_pgsql_hooked = cat_true;
    }
    return SUCCESS;
}

zend_result swow_pgsql_module_shutdown(INIT_FUNC_ARGS)
{
    if (swow_pgsql_hooked) {
        php_pdo_unregister_driver(&swow_pdo_pgsql_driver);

        // we cannot get original driver, so whatever
        // if (swow_original_pdo_pgsql) {
        //        php_pdo_register_driver(swow_original_pdo_pgsql);
        // }

        swow_pgsql_hooked = cat_false;
    }

    return SUCCESS;
}

#endif /* CAT_PQ */
